package com.opennews.openplatform.service;

import com.opennews.openplatform.entity.tables.records.UserActiveTokenRecord;
import com.opennews.openplatform.jooq.DSLContextWrapper;
import com.opennews.openplatform.myspringbootcore.security.MyUserDetails;
import com.opennews.openplatform.myspringbootcore.util.DateHelper;
import com.opennews.openplatform.myspringbootcore.util.SharedUtil;
import com.opennews.openplatform.security.SecurityActiveTokenManager;
import lombok.RequiredArgsConstructor;
import org.jooq.DSLContext;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

import static com.opennews.openplatform.entity.Tables.USER_ACTIVE_TOKEN;

@RequiredArgsConstructor
@Service
public class UserActiveTokenService {
    private final DSLContext dslContext;
    private final DSLContextWrapper dslContextWrapper;
    private final SecurityActiveTokenManager securityActiveTokenManager;

    /**
     * Queries all devices which logged in with provided username.
     *
     * @param username: The string of username.
     * @return The list of map instance which contains keys below.
     * id: String of id.
     * deviceId: String of device id.
     * deviceModel: String of device model.
     * enabled: Bool indicates this device is enabled / disabled.
     * lastUpdated: When data last updated.
     */
    public List<Map<String, Object>> queryDeviceByUsername(String username) {
        return dslContext.select(
                        USER_ACTIVE_TOKEN.ID.as("id"),
                        USER_ACTIVE_TOKEN.DEVICE_ID.as("deviceId"),
                        USER_ACTIVE_TOKEN.DEVICE_MODEL.as("deviceModel"),
                        USER_ACTIVE_TOKEN.ENABLED.as("enabled"),
                        USER_ACTIVE_TOKEN.LAST_UPDATED.as("lastUpdated")
                )
                .from(USER_ACTIVE_TOKEN)
                .where(USER_ACTIVE_TOKEN.USERNAME.eq(username))
                .orderBy(USER_ACTIVE_TOKEN.LAST_UPDATED.desc())
                .fetchMaps();
    }

    /**
     * Checks if this refresh token is still active. If the data does not exist means the token has been revoked.
     * Refresh token does not expire so it will not be cached like access token.
     * Because caching is for reducing database access during each api call where access token validation check is required.
     *
     * @param username:     The string of username.
     * @param refreshToken: The string of refresh token.
     * @return True means the token is active. False means not active.
     */
    public boolean isActiveRefreshToken(String username, String deviceId, String refreshToken) {
        var condition = USER_ACTIVE_TOKEN.USERNAME.eq(username)
                .and(USER_ACTIVE_TOKEN.REFRESH_TOKEN.eq(refreshToken))
                .and(USER_ACTIVE_TOKEN.ENABLED.isTrue());

        if (deviceId == null) {
            condition = condition.and(USER_ACTIVE_TOKEN.DEVICE_ID.isNull());
        } else {
            condition = condition.and(USER_ACTIVE_TOKEN.DEVICE_ID.eq(deviceId));
        }

        return dslContextWrapper.count(USER_ACTIVE_TOKEN, condition) > 0;
    }

    /**
     * Updates or inserts the access token and refresh token by username and deviceId.
     * This can track the same user login via different agent.
     * If you want to revoke the token just need to delete this row.
     *
     * @param myUserDetails: User details.
     * @param ipAddress      : String of client request ip address.
     * @param deviceInfo:    The map contains user device basic info.
     * @param systemInfo:    The map contains user device system info.
     */
    public boolean updateTokens(MyUserDetails myUserDetails, String ipAddress, Map<String, String> deviceInfo, Map<String, String> systemInfo) {
        var query = dslContext.select()
                .from(USER_ACTIVE_TOKEN)
                .where(
                        USER_ACTIVE_TOKEN.USERNAME.eq(myUserDetails.username)
                );

        if (deviceInfo != null && deviceInfo.get("id") != null) {
            query.and(USER_ACTIVE_TOKEN.DEVICE_ID.eq(deviceInfo.get("id")));
        } else {
            query.and(USER_ACTIVE_TOKEN.DEVICE_ID.isNull());
        }

        UserActiveTokenRecord activeToken = query.fetchOneInto(UserActiveTokenRecord.class);

        // If enabled == false means login from this device is forbidden.
        if (activeToken != null && !activeToken.getEnabled()) {
            return false;
        }

        if (activeToken == null) {
            activeToken = dslContext.newRecord(USER_ACTIVE_TOKEN);
            activeToken.setUsername(myUserDetails.username);
            activeToken.setEnabled(true);
        }

        LocalDateTime dateGenerated = DateHelper.getCurrentLocalDateTime();
        LocalDateTime dateExpired = dateGenerated.plusSeconds(myUserDetails.expiresIn);

        activeToken.setAccountGroupId(myUserDetails.accountGroupId);
        activeToken.setIpAddress(ipAddress);
        activeToken.setDeviceId(deviceInfo == null ? null : deviceInfo.get("id"));
        activeToken.setDeviceBrand(deviceInfo == null ? null : deviceInfo.get("brand"));
        activeToken.setDeviceModel(deviceInfo == null ? null : deviceInfo.get("model"));
        activeToken.setDeviceProductName(deviceInfo == null ? null : deviceInfo.get("productName"));
        activeToken.setDeviceSystemName(deviceInfo == null ? null : deviceInfo.get("systemName"));
        activeToken.setDeviceSystemVersion(deviceInfo == null ? null : deviceInfo.get("systemVersion"));
        activeToken.setOperatingSystem(systemInfo == null ? null : systemInfo.get("operatingSystem"));
        activeToken.setOperatingSystemVersion(systemInfo == null ? null : systemInfo.get("operatingSystemVersion"));
        activeToken.setPlatformVersion(systemInfo == null ? null : systemInfo.get("platformVersion"));
        activeToken.setAppVersion(systemInfo == null ? null : systemInfo.get("appVersion"));
        activeToken.setExpiration(myUserDetails.expiresIn);
        activeToken.setAccessToken(myUserDetails.accessToken);
        activeToken.setRefreshToken(myUserDetails.refreshToken);
        activeToken.setDateGenerated(dateGenerated);
        activeToken.setDateExpired(dateExpired);
        activeToken.store();

        // Stores the token in Redis with an expiration time if performance is critical.
        // Since refresh token does not expire then we should stick it in database then.
        // redisTemplate.opsForValue().set(myUserDetails.accessToken, myUserDetails.username, myUserDetails.expiresIn, TimeUnit.SECONDS);

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords();

        return true;
    }

    /**
     * Updates the enabled property for the user active token with provided id.
     *
     * @param id:      The string of id.
     * @param enabled: The boolean value of the enabled.
     */
    public void updateEnabled(String id, String username, boolean enabled) {
        dslContext.update(USER_ACTIVE_TOKEN)
                .set(USER_ACTIVE_TOKEN.ENABLED, enabled)
                .where(
                        USER_ACTIVE_TOKEN.ID.eq(id)
                                .and(USER_ACTIVE_TOKEN.USERNAME.eq(username))
                )
                .execute();

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords();
    }

    /**
     * Deletes active data by access token.
     *
     * @param accessToken: The string of access token.
     */
    public void deleteByAccessToken(String accessToken) {
        if (!SharedUtil.isNullOrEmpty(accessToken)) {
            dslContext.deleteFrom(USER_ACTIVE_TOKEN)
                    .where(USER_ACTIVE_TOKEN.ACCESS_TOKEN.eq(accessToken))
                    .execute();

            // Removes from Redis
            // redisTemplate.delete(accessToken);

            securityActiveTokenManager.reloadCachedUserActiveTokenRecords();
        }
    }

    /**
     * Deletes active data by id and username.
     *
     * @param id:       The string of id.
     * @param username: The string of id.
     */
    public void delete(String id, String username) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
                .where(
                        USER_ACTIVE_TOKEN.ID.eq(id)
                                .and(USER_ACTIVE_TOKEN.USERNAME.eq(username))
                )
                .execute();

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords();
    }

    public void deleteByUserAndAccountGroup(String username, String accountGroupId) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
                .where(USER_ACTIVE_TOKEN.USERNAME.eq(username)
                        .and(USER_ACTIVE_TOKEN.ACCOUNT_GROUP_ID.eq(accountGroupId)))
                .execute();

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords();
    }

    public void deleteByAccountGroup(String accountGroupId) {
        dslContext.deleteFrom(USER_ACTIVE_TOKEN)
                .where(USER_ACTIVE_TOKEN.ACCOUNT_GROUP_ID.eq(accountGroupId))
                .execute();

        securityActiveTokenManager.reloadCachedUserActiveTokenRecords();
    }
}
